﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Diagnostics;
using System.Threading.Tasks;
using Common.Logging;

namespace Samurai.Wakizashi.Workflow
{
    /// <summary>
    /// Defines a workflow that executes steps in sequential order.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class SequentialWorkflow<T> : ISequentialWorkflow<T> where T : class, IWorkflowContext
    {
        #region Fields

        // ReSharper disable StaticFieldInGenericType
        protected readonly ILog Logger = LogManager.GetLogger(typeof (SequentialWorkflow<T>));
        private readonly Queue<IWorkflowStep<T>> _underlyingQueue;
        private IWorkflowStep<T>[] _allSteps;
        private bool _stopFlagSet;
        private static object uiThreadDispatcher;
        private static MethodInfo dispatcherBeginInvokeMethodInfo;
        private Action<WorkflowCompletedEventArgs<T>> _completedAction;
        private Action<WorkflowCompletedEventArgs<T>> _failedAction;
        // ReSharper restore StaticFieldInGenericType

        #endregion

        #region Constructors

        public SequentialWorkflow() : this(default(T)) {}

        /// <summary>
        /// Initializes a new instance of the <see cref="SequentialWorkflow&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="context">The context.</param>
        public SequentialWorkflow(T context)
        {
            _underlyingQueue = new Queue<IWorkflowStep<T>>();
            Context = context;
        }

        #endregion

        #region Public Members

        /// <summary>
        /// Occurs when the final step in the workflow has completed.
        /// </summary>
        public event EventHandler<WorkflowCompletedEventArgs<T>> WorkflowCompleted;

        /// <summary>
        /// Occurs when a step in the workflow fails.
        /// </summary>
        public event EventHandler<WorkflowCompletedEventArgs<T>> WorkflowFailed;

        /// <summary>
        /// Gets or sets the context which to pass along the workflow as it executes.
        /// </summary>
        /// <value>The context.</value>
        public T Context { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether or not to restart the workflow after the last step completes.
        /// </summary>
        /// <value><c>true</c> if [loop workflow]; otherwise, <c>false</c>.</value>
        public bool LoopWorkflow { get; set; }

        /// <summary>
        /// Gets the steps that make up the current workflow instance.
        /// </summary>
        /// <value>The workflow steps.</value>
        public IEnumerable<IWorkflowStep<T>> WorkflowSteps
        {
            get { return _underlyingQueue.ToArray(); }
        }

        /// <summary>
        /// Gets the number of steps registered in the workflow.
        /// </summary>
        /// <value>The count.</value>
        public int Count
        {
            get
            {
                return _underlyingQueue.Count;
            }
        }

        /// <summary>
        /// Starts the workflow by executing the first step.
        /// </summary>
        public void Start()
        {
            _underlyingQueue.TrimExcess();

            // Store a copy of all the steps registered in case LoopWorkflow is set to true and we need 
            //  to re-queue the steps and play them again
            _allSteps = _underlyingQueue.ToArray();

            if(Logger.IsDebugEnabled) Logger.DebugFormat("Workflow starting with {0} steps registered", _underlyingQueue.Count);

            AdvanceToNextExecutableStep();
        }

        #region RegisterStep Overloads

        /// <summary>
        /// Registers the provided step with the workflow.
        /// </summary>
        /// <param name="step">The step.</param>
        public void RegisterStep(IWorkflowStep<T> step)
        {
            RegisterStep(step, ThreadType.Current);
        }

        /// <summary>
        /// Registers the provided step with the workflow. Will execute if provided predicate evaluates to true at execution time. Will
        /// execute on the current thread.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="canExecute">Execution condition.</param>
        public void RegisterStep(IWorkflowStep<T> step, Predicate<T> canExecute)
        {
            RegisterStep(step, ThreadType.Current, canExecute);
        }

        /// <summary>
        /// Registers the provided step with the workflow on the specified thread in which to execute.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        public void RegisterStep(IWorkflowStep<T> step, ThreadType thread)
        {
            RegisterStep(step, thread, null);
        }

        /// <summary>
        /// Registers the provided step with the workflow on the specified thread in which to execute. Will 
        /// execute if provided predicate evaluates to true at execution time.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        /// <param name="canExecute">Execution condition.</param>
        public void RegisterStep(IWorkflowStep<T> step, ThreadType thread, Predicate<T> canExecute)
        {
            step.Workflow = this;
            step.Thread = thread;
            step.Context = Context;
            step.ExecutionPredicate = canExecute;
            step.Completed += Step_Completed;
            step.Failed += Step_Failed;

            _underlyingQueue.Enqueue(step);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow to be executed on the current thread.
        /// </summary>
        /// <param name="step">The step.</param>
        public void RegisterStep(Action step)
        {
            RegisterStep(step, ThreadType.Current, null, null);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        public void RegisterStep(Action step, ThreadType thread)
        {
            RegisterStep(step, thread, null, null);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute. Will 
        /// only execute the step if execution condition evaluates to 'true' when the step is next in the workflow.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="canExecute">Execution condition.</param>
        public void RegisterStep(Action step, Predicate<T> canExecute)
        {
            RegisterStep(step, ThreadType.Current, canExecute, null);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute. Will
        /// only execute the step if execution condition evaluates to 'true' when the step is next in the workflow.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        /// <param name="canExecute">Execution condition.</param>
        /// <param name="stepName">Name of the step.</param>
        public void RegisterStep(Action step, ThreadType thread, Predicate<T> canExecute, string stepName)
        {
            var delegatedStep = new DelegatedWorkflowStep<T>(step, stepName) { Context = Context };
            RegisterStep(delegatedStep, thread, canExecute);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow to be executed on the current thread.
        /// </summary>
        /// <param name="step">The step.</param>
        public void RegisterStep(Action<T> step)
        {
            RegisterStep(step, ThreadType.Current, null, null);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        public void RegisterStep(Action<T> step, ThreadType thread)
        {
            RegisterStep(step, thread, null, null);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute. Will 
        /// only execute the step if execution condition evaluates to 'true' when the step is next in the workflow.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="canExecute">Execution condition.</param>
        public void RegisterStep(Action<T> step, Predicate<T> canExecute)
        {
            RegisterStep(step, ThreadType.Current, canExecute, null);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute. Will
        /// only execute the step if execution condition evaluates to 'true' when the step is next in the workflow.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        /// <param name="canExecute">Execution condition.</param>
        /// <param name="stepName">Name of the step.</param>
        public void RegisterStep(Action<T> step, ThreadType thread, Predicate<T> canExecute, string stepName)
        {
            var delegatedStep = new DelegatedWorkflowStep<T>(step, stepName) { Context = Context };
            RegisterStep(delegatedStep, thread, canExecute);
        }

        #endregion

        /// <summary>
        /// Suspends the current workflow for the specified amount of time. Once this time 
        /// has elapsed, the workflow will continue to the next step.
        /// </summary>
        /// <param name="amountOfTimeToSuspend">The amount of time to suspend.</param>
        public void Suspend(TimeSpan amountOfTimeToSuspend)
        {
            var suspendStep = new SuspendWorkflowStep<T>(amountOfTimeToSuspend);
            RegisterStep(suspendStep, ThreadType.Current);
        }

        /// <summary>
        /// Places a request to stop the workflow instance at the next possible point.
        /// </summary>
        public void RequestStop()
        {
            _stopFlagSet = true;

            if (Logger.IsDebugEnabled) Logger.Debug("Workflow forcibly stopped");
        }

        /// <summary>
        /// Configures the specified config action.
        /// </summary>
        /// <param name="configAction">The config action.</param>
        public void Configure(Action<ISequentialWorkflow<T>> configAction)
        {
            if (configAction != null)
                configAction(this);
        }

        /// <summary>
        /// Called when the final step in the workflow has completed.
        /// </summary>
        public void OnCompleted(Action<WorkflowCompletedEventArgs<T>> action)
        {
            _completedAction = action;
        }

        /// <summary>
        /// Calledwhen a step in the workflow fails.
        /// </summary>
        public void OnFailed(Action<WorkflowCompletedEventArgs<T>> action)
        {
            _failedAction = action;
        }

        /// <summary>
        /// Clears all steps from the workflow.
        /// </summary>
        public void Clear()
        {
            _underlyingQueue.Clear();
        }

        #endregion

        #region Protected Members

        /// <summary>
        /// Notifies interested parties that the workflow failed.
        /// </summary>
        /// <param name="eventArgs">The <see cref="Samurai.Wakizashi.Workflow.WorkflowCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected void NotifyWorkflowFailed(WorkflowCompletedEventArgs<T> eventArgs)
        {
            if (WorkflowFailed != null)
                WorkflowFailed(this, eventArgs);

            if (_failedAction != null)
                _failedAction(eventArgs);

            var stepName = (eventArgs.FailedWorkflowStep != null)
                               ? GetFailedStepName(eventArgs.FailedWorkflowStep)
                               : "Unknown";
            var exceptionDetails = (eventArgs.Exception != null) ? eventArgs.Exception.ToString() : "Unknown";
            Logger.Error(string.Format("Workflow failed on step: {0}\r\nException: {1}", stepName, exceptionDetails));
        }

        private string GetFailedStepName(IWorkflowStep<T> failedWorkflowStep)
        {
            if(!string.IsNullOrEmpty(failedWorkflowStep.StepName))
                return failedWorkflowStep.StepName;

            return failedWorkflowStep.GetType().FullName;
        }

        /// <summary>
        /// Notifies interested parties that the workflow completed.
        /// </summary>
        /// <param name="eventArgs">The <see cref="Samurai.Wakizashi.Workflow.WorkflowCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected void NotifyWorkflowCompleted(WorkflowCompletedEventArgs<T> eventArgs)
        {
            if (WorkflowCompleted != null)
                WorkflowCompleted(this, eventArgs);

            if (_completedAction != null)
                _completedAction(eventArgs);

            if(Logger.IsInfoEnabled) Logger.Info("Workflow completed.");
        }

        // ReSharper disable InconsistentNaming
        /// <summary>
        /// Handles the Completed event of the Step control by executing the next step in the workflow.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="Samurai.Wakizashi.Workflow.WorkflowStepCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>        
        protected virtual void Step_Completed(object sender, WorkflowStepCompletedEventArgs<T> e)
        {
            var step = (IWorkflowStep<T>)sender;

            if(!LoopWorkflow)
                DetachStepEvents(step);

            if (Logger.IsDebugEnabled) Logger.DebugFormat("Step completed: {0}", step.StepName);

            //More workflow steps in the queue...continue exection.
            AdvanceToNextExecutableStep();
        }

        /// <summary>
        /// Handles the Failed event of the Step instance.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="Samurai.Wakizashi.Workflow.WorkflowStepCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected virtual void Step_Failed(object sender, WorkflowStepCompletedEventArgs<T> e)
        {
            if(!LoopWorkflow)
                DetachStepEvents(e.FailedWorkflowStep);

            if (e.FailedWorkflowStep.IsCritical)
            {
                NotifyWorkflowFailed(new WorkflowCompletedEventArgs<T>
                {
                    Context = Context,
                    Exception = e.Exception,
                    FailedWorkflowStep = e.FailedWorkflowStep
                });
                return;
            }

            AdvanceToNextExecutableStep();
        }
        // ReSharper restore InconsistentNaming

        #endregion

        #region Private Methods

        private void AdvanceToNextExecutableStep()
        {
            // First stop condition is if a stop has been requested
            if(_stopFlagSet)
            {
                NotifyWorkflowCompleted(new WorkflowCompletedEventArgs<T> { Context = Context });
                return;
            }

            // Second stop condition is if we have no more steps
            if(_underlyingQueue.Count == 0)
            {
                NotifyWorkflowCompleted(new WorkflowCompletedEventArgs<T> { Context = Context });

                if(LoopWorkflow)
                    Loop();

                return;
            }

            // Third stop condition is if we have a step without a predicate to execute
            var step = _underlyingQueue.Dequeue();
            if (!step.IsConditional)
            {
                ExecuteStep(step);
                return;
            }

            // Fourth stop condition is if we have a step with positive predicate
            if (step.ExecutionPredicate.Invoke(Context))
            {
                ExecuteStep(step);
                return;
            }

            // No stop conditions met. Recurse until we hit one.
            AdvanceToNextExecutableStep();
        }

        private void Loop()
        {
            if (Logger.IsDebugEnabled) Logger.Debug("LoopWorkflow is set to true. Workflow is restarting.");

            if (_allSteps == null || _allSteps.Length == 0)
            {
                Logger.Warn("No steps saved for this workflow. Cannot re-queue original steps and loop the workflow.");
                return;
            }

            foreach (var step in _allSteps)
            {
                step.ResetCompletedFlag();
                _underlyingQueue.Enqueue(step);
            }

            Start();
        }

        private static void ExecuteStep(IWorkflowStep<T> step)
        {
            switch (step.Thread)
            {
                case ThreadType.Current:
                    step.ExecuteStep();
                    break;
                case ThreadType.Task:
                    Task.Factory.StartNew(step.ExecuteStep);
                    break;
                case ThreadType.ThreadPool:
                    ThreadPool.QueueUserWorkItem(x => step.ExecuteStep());
                    break;
                case ThreadType.Ui:
                    ResolveUiThreadDispatcher();

                    if (dispatcherBeginInvokeMethodInfo == null)
                    {
                        //Not a WPF app, so execute on the current thread
                        Trace.WriteLine("No WPF application present from which to use UI thread. Defaulting to ThreadType.Current.");
                        step.ExecuteStep();
                        return;
                    }

                    var threadStart = new ThreadStart(step.ExecuteStep);
                    var parameters = new object[] { threadStart, null };
                    dispatcherBeginInvokeMethodInfo.Invoke(uiThreadDispatcher, parameters);

                    break;
                default:
                    throw new InvalidOperationException("BeginWork(), firstItem.ThreadToExecuteOn");
            }
        }

        private static void ResolveUiThreadDispatcher()
        {
            if(dispatcherBeginInvokeMethodInfo != null) return;

            const string applicationTypeName = "System.Windows.Application";
            const string presentationFrameworkAssembly = "PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35";
            const string applicationInstancePath = "Current";
            const string dispatcherPath = "Dispatcher";

            var applicationType = Type.GetType(string.Format("{0}, {1}", applicationTypeName, presentationFrameworkAssembly), false, false);
            var currentPropertyInfo = applicationType.GetProperty(applicationInstancePath, (BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy));
            var app = currentPropertyInfo.GetValue(null, null);

            if (app == null) return;

            var dispatcherPropertyInfo = applicationType.GetProperty(dispatcherPath);
            uiThreadDispatcher = dispatcherPropertyInfo.GetValue(app, null);

            if (uiThreadDispatcher == null) return;

            dispatcherBeginInvokeMethodInfo = uiThreadDispatcher.GetType().GetMethod("BeginInvoke", new[] { typeof(Delegate), typeof(object[])});
        }

        private void DetachStepEvents(IWorkflowStep<T> step)
        {
            step.Completed -= Step_Completed;
            step.Failed -= Step_Failed;
        }

        #endregion
    }
}
